#include "parser.h"

#include <stdbool.h>
#include <wctype.h>
#include <wchar.h>
#include "stack.h"
#include "utils/assert.h"
#include "utils/logger.h"

typedef enum
{
    #define ESTATE_ENUM(x) EState_##x,
    #include "EState.inc"
    #undef ESTATE_ENUM

} EState;

const char *EStateToStr(EState s)
{
    switch (s)
    {
        #define ESTATE_ENUM(x) case EState_##x: return #x;
        #include "EState.inc"
        #undef ESTATE_ENUM
    }
    return "INVALID VALUE OF EState";
}

void changeStateValue(EState *toChange, const EState newValue)
{
    if (newValue == *toChange)
    {
        return;
    }
    LOG_DEBUG("Changing EState value from %s to %s", EStateToStr(*toChange), EStateToStr(newValue));
    *toChange = newValue;
}

EState parseBeforeTag(wint_t c);
EState parseTagStart(wint_t c);
EState parseTagName(wint_t c);
EState parseClosingTagName(wint_t c);
EState parseTagWhiteSpace(wint_t c);
EState parseTagAttributeName(wint_t c);
EState parseTagAttributeAssignment(wint_t c);
EState parseTagAttributeOneWordValue(wint_t c);
EState parseTagAttributeLongValueBegin(wint_t c);
EState parseTagAttributeLongValue(wint_t c);
EState parseTagAttributeLongValueEnd(wint_t c);
EState parseText(wint_t c);
EState parseClosingNoEndTag(wint_t c);

// TODO: Spaghetti code. Refactor it. Maybe some magic macro code generation?

void cleanUp(StackNode *tagStack)
{
    stack_delete(tagStack);
}

bool isStackConsistent(const StackNode *top, const wchar_t *expectedTopData)
{
    if (!top)
    {
        LOG_ERROR("Want to close <%ls> but the stack is empty", expectedTopData);
        return false;
    }
    if (0 != wcscmp(top->data, expectedTopData))
    {
        LOG_ERROR("Want to close <%ls> but the last one on a stack is <%ls>", expectedTopData, top->data);
        return false;
    }
    return true;
}

bool doesOpenedTagRequireEndTag(const wchar_t *tagName)
{
    return 0 != wcscmp(tagName, L"!DOCTYPE");
}

HtmlDocument* parse(FILE *stream)
{
    ASSERT_MSG(stream != NULL, "NULL pointer provided");

    EState state = EState_BeforeTag;

    StackNode *tagStack = NULL;
    wchar_t buffer[1024*50];
    size_t bufferPosition = 0;

    wint_t c;
    while ((c = fgetwc(stream)) != WEOF)
    {
        // LOG_DEBUG("%lc", c);
        switch (state)
        {
            case EState_BeforeTag:
            {
                EState newState = parseBeforeTag(c);
                if (EState_ParsingText == newState)
                {
                    ASSERT(bufferPosition == 0);
                    buffer[bufferPosition++] = c;
                }
                changeStateValue(&state, newState);
                break;
            }
            case EState_ParsingTagStart:
            {
                EState newState = parseTagStart(c);
                if (EState_Error == newState)
                {
                    LOG_ERROR("Error during parsing");
                    cleanUp(tagStack);
                    return NULL;
                }
                if (EState_ParsingTagName == newState)
                {
                    ASSERT(bufferPosition == 0);
                    buffer[bufferPosition++] = c;
                }
                else if (EState_ParsingClosingTagName == newState)
                {
                    ASSERT(bufferPosition == 0);
                }
                changeStateValue(&state, newState);
                break;
            }
            case EState_ParsingTagName:
            {
                EState newState = parseTagName(c);
                if (newState == EState_ParsingTagName)
                {
                    buffer[bufferPosition++] = c;
                }
                else
                {
                    buffer[bufferPosition++] = '\0';
                    LOG_INFO("Opening tag: '%ls'", buffer);
                    tagStack = stack_push(tagStack, buffer);
                    bufferPosition = 0;
                    changeStateValue(&state, newState);

                    if (!doesOpenedTagRequireEndTag(buffer))
                    {
                        LOG_DEBUG("Special tag, doesn't require closing tag: <%ls>", buffer);
                        tagStack = stack_pop(tagStack);
                    }
                }
                break;
            }
            case EState_ParsingClosingTagName:
            {
                EState newState = parseClosingTagName(c);
                if (newState == EState_ParsingClosingTagName)
                {
                    buffer[bufferPosition++] = c;
                }
                else
                {
                    buffer[bufferPosition++] = '\0';
                    LOG_INFO("Closing tag: '%ls'", buffer);
                    bufferPosition = 0;
                    if (!isStackConsistent(tagStack, buffer))
                    {
                        LOG_ERROR("Tag stack inconsistency detected");
                        cleanUp(tagStack);
                        return NULL;
                    }
                    tagStack = stack_pop(tagStack);
                    changeStateValue(&state, newState);
                }
                break;
            }
            case EState_ParsingTagWhiteSpace:
            {
                EState newState = parseTagWhiteSpace(c);
                changeStateValue(&state, newState);
                break;
            }
            case EState_ParsingTagAttributeName:
            {
                EState newState = parseTagAttributeName(c);
                changeStateValue(&state, newState);
                break;
            }
            case EState_ParsingTagAttributeAssignment:
            {
                EState newState = parseTagAttributeAssignment(c);
                changeStateValue(&state, newState);
                break;
            }
            case EState_ParsingTagAttributeOneWordValue:
            {
                EState newState = parseTagAttributeOneWordValue(c);
                changeStateValue(&state, newState);
                break;
            }
            case EState_ParsingTagAttributeLongValueBegin:
            {
                EState newState = parseTagAttributeLongValueBegin(c);
                changeStateValue(&state, newState);
                break;
            }
            case EState_ParsingTagAttributeLongValue:
            {
                EState newState = parseTagAttributeLongValue(c);
                changeStateValue(&state, newState);
                break;
            }
            case EState_ParsingTagAttributeLongValueEnd:
            {
                EState newState = parseTagAttributeLongValueEnd(c);
                changeStateValue(&state, newState);
                break;
            }
            case EState_ParsingText:
            {
                EState newState = parseText(c);
                if (newState == EState_ParsingText)
                {
                    buffer[bufferPosition++] = c;
                }
                else
                {
                    buffer[bufferPosition++] = '\0';
                    LOG_INFO("TEXT: '%ls'", buffer);
                    bufferPosition = 0;
                    changeStateValue(&state, newState);
                }
                break;
            }
            case EState_ClosingNoEndTag:
            {
                ASSERT(0 == bufferPosition);
                if (!isStackConsistent(tagStack, buffer))
                {
                    LOG_ERROR("Tag stack inconsistency detected");
                    cleanUp(tagStack);
                    return NULL;
                }
                tagStack = stack_pop(tagStack);
                EState newState = parseClosingNoEndTag(c);
                if (EState_Error == newState)
                {
                    LOG_ERROR("Invalid syntax for closing no-end <tag/>");
                    cleanUp(tagStack);
                    return NULL;
                }
                changeStateValue(&state, newState);
                break;
            }
            case EState_Error:
            {
                LOG_ERROR("Error during last operation");
                cleanUp(tagStack);
                return NULL;
            }
            default:
            {
                LOG_ERROR("Unhandled state: %s", EStateToStr(state));
                cleanUp(tagStack);
                return NULL;
            }
        }
    }

    if (state != EState_BeforeTag)
    {
        LOG_ERROR("End of file before parsing finish");
        cleanUp(tagStack);
        return NULL;
    }
    if (tagStack)
    {
        LOG_ERROR("Some tags were not closed:");
        StackNode *tmp = tagStack;
        while (tmp)
        {
            LOG_ERROR("<%ls>", tmp->data);
            tmp = tmp->previous;
        }
        cleanUp(tagStack);
        return NULL;
    }

    LOG_DEBUG("Html document parsed correctly");
    return NULL;
}

EState parseBeforeTag(wint_t c)
{
    if ('<' != c)
    {
        return EState_ParsingText;
    }
    return EState_ParsingTagStart;
}

EState parseText(wint_t c)
{
    if ('<' != c)
    {
        return EState_ParsingText;
    }
    return EState_ParsingTagStart;
}

EState parseTagStart(wint_t c)
{
    if ('/' == c)
    {
        return EState_ParsingClosingTagName;
    }
    if (!iswalnum(c) && '!' != c)
    {
        LOG_ERROR("Invalid char in tag name: %lc", c);
        return EState_Error;
    }
    return EState_ParsingTagName;
}

EState parseTagName(wint_t c)
{
    if (iswalnum(c))
    {
        return EState_ParsingTagName;
    }
    if ('/' == c)
    {
        return EState_ClosingNoEndTag;
    }
    if ('>' == c)
    {
        return EState_BeforeTag;
    }
    if (' ' == c)
    {
        return EState_ParsingTagWhiteSpace;
    }
    LOG_ERROR("Invalid char '%lc'", c);
    return EState_Error;
}

EState parseClosingTagName(wint_t c)
{
    if (iswalnum(c))
    {
        return EState_ParsingClosingTagName;
    }
    if ('>' == c)
    {
        return EState_BeforeTag;
    }
    LOG_ERROR("Invalid char '%lc'", c);
    return EState_Error;
}

EState parseTagWhiteSpace(wint_t c)
{
    if (' ' == c)
    {
        return EState_ParsingTagWhiteSpace;
    }
    if ('/' == c)
    {
        return EState_ClosingNoEndTag;
    }
    if ('>' == c)
    {
        return EState_BeforeTag;
    }
    if (iswalnum(c))
    {
        return EState_ParsingTagAttributeName;
    }
    LOG_ERROR("Invalid char '%lc'", c);
    return EState_Error;
}

EState parseTagAttributeName(wint_t c)
{
    if (' ' == c)
    {
        return EState_ParsingTagWhiteSpace;
    }
    if ('/' == c)
    {
        return EState_ClosingNoEndTag;
    }
    if ('>' == c)
    {
        return EState_BeforeTag;
    }
    if ('=' == c)
    {
        return EState_ParsingTagAttributeAssignment;
    }
    if (iswalnum(c))
    {
        return EState_ParsingTagAttributeName;
    }
    LOG_ERROR("Invalid char '%lc'", c);
    return EState_Error;
}

EState parseTagAttributeAssignment(wint_t c)
{
    if ('\"' == c)
    {
        return EState_ParsingTagAttributeLongValueBegin;
    }
    return EState_ParsingTagAttributeOneWordValue;
}

EState parseTagAttributeOneWordValue(wint_t c)
{
    if (' ' == c)
    {
        return EState_ParsingTagWhiteSpace;
    }
    if ('/' == c)
    {
        return EState_ClosingNoEndTag;
    }
    if ('>' == c)
    {
        return EState_BeforeTag;
    }
    return EState_ParsingTagAttributeOneWordValue;
}

EState parseTagAttributeLongValueBegin(wint_t c)
{
    if ('\"' == c)
    {
        return EState_ParsingTagAttributeLongValueEnd;
    }
    return EState_ParsingTagAttributeLongValue;
}

EState parseTagAttributeLongValue(wint_t c)
{
    if ('\"' == c)
    {
        return EState_ParsingTagAttributeLongValueEnd;
    }
    return EState_ParsingTagAttributeLongValue;
}

EState parseTagAttributeLongValueEnd(wint_t c)
{
    if (' ' == c)
    {
        return EState_ParsingTagWhiteSpace;
    }
    if ('/' == c)
    {
        return EState_ClosingNoEndTag;
    }
    if ('>' == c)
    {
        return EState_BeforeTag;
    }
    LOG_ERROR("Invalid char '%lc'", c);
    return EState_Error;
}

EState parseClosingNoEndTag(wint_t c)
{
    if ('>' == c)
    {
        return EState_BeforeTag;
    }
    LOG_ERROR("Invalid char '%lc'", c);
    return EState_Error;
}